﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.Serialization.Json;
using System.Windows.Input;
using EuroManager.Presentation.WebPortal.GameViewer.Data;
using EuroManager.Presentation.WebPortal.GameViewer.Models;
using EuroManager.Presentation.WebPortal.GameViewer.Updates;
using EuroManager.Presentation.WebPortal.GameViewer.Utilities;

namespace EuroManager.Presentation.WebPortal.GameViewer
{
    public class MainViewModel : INotifyPropertyChanged, ISpeedController
    {
        private static readonly float normalSpeed = 1f;
        
        private GameData result = null;
        private Clock clock = null;
        private Score score = null;
        private Ball ball = null;
        private IEnumerable<Player> players = null;

        private Dictionary<int, IList<IGameUpdate>> updatesByFrame = new Dictionary<int, IList<IGameUpdate>>();
        
        private bool hasStarted = false;
        private bool isActive = false;
        private bool isPlaying = false;
        private bool isPaused = false;
        private bool isRewinding = false;
        private float speed = normalSpeed;

        private int previousFrameIndex = -1;
        private DateTime previousTime;
        private float elapsedTime;

        private int ballFrameCount;
        private int playerFrameCount;
        private float ballFrameTime;
        private float playerFrameTime;

        public MainViewModel()
        {
            StartCommand = new DelegateCommand<string>(s => Start(s));
            PlayCommand = new DelegateCommand<object>(o => Play());
            PauseCommand = new DelegateCommand<object>(o => Pause());
            RewindCommand = new DelegateCommand<object>(o => Rewind());
            UpdateFrameCommand = new DelegateCommand<object>(o => UpdateFrame());
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public ICommand StartCommand { get; private set; }

        public ICommand PauseCommand { get; private set; }

        public ICommand RewindCommand { get; private set; }

        public ICommand PlayCommand { get; private set; }

        public ICommand UpdateFrameCommand { get; private set; }

        public bool HasStarted
        {
            get { return hasStarted; }
            private set
            {
                hasStarted = value;
                RaisePropertyChanged("HasStarted");
            }
        }

        public bool IsActive
        {
            get { return isActive; }
            private set
            {
                isActive = value;
                RaisePropertyChanged("IsActive");
            }
        }

        public bool IsPlaying
        {
            get { return isPlaying; }
            private set
            {
                isPlaying = value;
                RaisePropertyChanged("IsPlaying");
            }
        }

        public bool IsPaused
        {
            get { return isPaused; }
            private set
            {
                isPaused = value;
                RaisePropertyChanged("IsPaused");
            }
        }

        public bool IsRewinding
        {
            get { return isRewinding; }
            private set
            {
                isRewinding = value;
                RaisePropertyChanged("IsRewinding");
            }
        }

        public float Speed
        {
            get { return speed; }
            set
            {
                speed = value;
                RaisePropertyChanged("Speed");
            }
        }

        public Clock Clock
        {
            get { return clock; }
            private set
            {
                clock = value;
                RaisePropertyChanged("Clock");
            }
        }

        public Score Score
        {
            get { return score; }
            private set
            {
                score = value;
                RaisePropertyChanged("Score");
            }
        }

        public Ball Ball
        {
            get { return ball; }
            private set
            {
                ball = value;
                RaisePropertyChanged("Ball");
            }
        }

        public IEnumerable<Player> Players
        {
            get { return players; }
            private set
            {
                players = value;
                RaisePropertyChanged("Players");
            }
        }

        private void Start(string gameData)
        {
            result = LoadGameResult(gameData);
            CreateModels(result);
            CreateGameUpdates(result, updatesByFrame);

            previousTime = DateTime.Now;
            HasStarted = true;
            IsActive = true;
            Play();
        }

        private GameData LoadGameResult(string gameData)
        {
            GameData result;
            byte[] data = Convert.FromBase64String(gameData);

            using (MemoryStream stream = new MemoryStream(data))
            {
                DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(GameData));
                result = (GameData)serializer.ReadObject(stream);
            }

            ballFrameCount = (int)(result.FrameCount / ((float)result.Fps / result.BallFps));
            playerFrameCount = (int)(result.FrameCount / ((float)result.Fps / result.PlayerFps));
            ballFrameTime = 1.0f / result.BallFps;
            playerFrameTime = 1.0f / result.PlayerFps;

            return result;
        }

        private void CreateModels(GameData result)
        {
            Clock = new Clock();
            Score = new Score();
            Ball = new Ball();
            Players = result.Players.Select(p => new Player(p.Team, p.Number)).ToArray();
        }

        private void CreateGameUpdates(GameData result, IDictionary<int, IList<IGameUpdate>> updates)
        {
            for (int frameIndex = 0; frameIndex < result.FrameCount; frameIndex++)
            {
                updates[frameIndex] = new List<IGameUpdate>();
            }

            CreateClockUpdates(result, updates);
            ////CreateScoreUpdates(result, updates);
            ////CreateSpeedUpdates(result, updates);
            CreateBallUpdates(result, updates);
            CreatePlayerUpdates(result, updates);
        }

        private void CreateClockUpdates(GameData result, IDictionary<int, IList<IGameUpdate>> updates)
        {
            for (int i = 0; i < result.TimeUpdates.Length; i++)
            {
                int frameIndex = result.TimeUpdates[i];
                updates[frameIndex].Add(new ClockUpdate(clock, TimeSpan.FromSeconds(i + 1)));
            }
        }

        ////private void CreateSpeedUpdates(GameData result, IDictionary<int, IList<IGameUpdate>> updates)
        ////{
        ////    for (int i = 0; i < result.Highlights.Length; i++)
        ////    {
        ////        HighlightData highlight = result.Highlights[i];
        ////        updates[highlight.StartFrame].Add(new SpeedUpdate(this, normalSpeed));
        ////    }
        ////}

        ////private void CreateScoreUpdates(GameData result, IDictionary<int, IList<IGameUpdate>> updates)
        ////{
        ////    for (int i = 0; i < result.Score1Updates.Length; i++)
        ////    {
        ////        int frameIndex = result.Score1Updates[i];
        ////        updates[frameIndex].Add(new ScoreUpdate(score, GameTeam.Team1, i + 1));
        ////    }

        ////    for (int i = 0; i < result.Score2Updates.Length; i++)
        ////    {
        ////        int frameIndex = result.Score2Updates[i];
        ////        updates[frameIndex].Add(new ScoreUpdate(score, GameTeam.Team2, i + 1));
        ////    }
        ////}

        private void CreateBallUpdates(GameData result, IDictionary<int, IList<IGameUpdate>> updates)
        {
            for (int frameIndex = 0; frameIndex < result.FrameCount; frameIndex++)
            {
                updates[frameIndex].Add(CreateBallLocationUpdate(result, frameIndex));
            }
        }

        private IGameUpdate CreateBallLocationUpdate(GameData result, int frameIndex)
        {
            int ballFrameIndex = (int)(frameIndex / ((float)result.Fps / result.BallFps));

            if (ballFrameIndex < 0 || ballFrameCount <= ballFrameIndex + 1)
            {
                return new EmptyUpdate();
            }
            else
            {
                return new BallLocationUpdate(
                    ball,
                    ballFrameTime,
                    result.Ball.X[ballFrameIndex],
                    result.Ball.Y[ballFrameIndex],
                    result.Ball.X[ballFrameIndex + 1],
                    result.Ball.Y[ballFrameIndex + 1]);
            }
        }

        private void CreatePlayerUpdates(GameData result, IDictionary<int, IList<IGameUpdate>> updates)
        {
            for (int frameIndex = 0; frameIndex < result.FrameCount; frameIndex++)
            {
                int playerIndex = 0;

                foreach (Player player in players)
                {
                    updates[frameIndex].Add(CreatePlayerLocationUpdate(player, result.Players[playerIndex], frameIndex));
                    playerIndex++;
                }
            }
        }

        private IGameUpdate CreatePlayerLocationUpdate(Player player, PlayerData playerData, int frameIndex)
        {
            int playerFrameIndex = (int)(frameIndex / ((float)result.Fps / result.PlayerFps));

            if (playerFrameIndex < 0 || playerFrameCount <= playerFrameIndex + 1)
            {
                return new EmptyUpdate();
            }
            else
            {
                return new PlayerLocationUpdate(
                    player,
                    playerFrameTime,
                    playerData.X[playerFrameIndex],
                    playerData.Y[playerFrameIndex],
                    playerData.X[playerFrameIndex + 1],
                    playerData.Y[playerFrameIndex + 1],
                    playerData.Angle[playerFrameIndex],
                    playerData.Angle[playerFrameIndex + 1]);
            }
        }

        private void Play()
        {
            IsPlaying = true;
            IsPaused = false;
            IsRewinding = false;
        }

        private void Pause()
        {
            IsPlaying = false;
            IsPaused = true;
            IsRewinding = false;
        }

        private void Rewind()
        {
            IsPlaying = false;
            IsPaused = false;
            IsRewinding = true;
        }

        private void UpdateFrame()
        {
            if (IsActive)
            {
                if (IsPaused)
                {
                    previousTime = DateTime.Now;
                }
                else
                {
                    DateTime now = DateTime.Now;
                    elapsedTime += (float)(now - previousTime).TotalSeconds * (IsRewinding ? -Speed : Speed);
                    previousTime = now;

                    if (elapsedTime < 0)
                    {
                        elapsedTime = 0;
                    }

                    int frameIndex = (int)(elapsedTime * result.Fps);
                    UpdateToFrame(frameIndex);
                }
            }
        }

        private void UpdateToFrame(int frameIndex)
        {
            if (frameIndex >= result.FrameCount)
            {
                IsActive = false;
            }
            else if (frameIndex >= 0)
            {
                while ((IsRewinding && previousFrameIndex > frameIndex) ||
                    (!IsRewinding && previousFrameIndex < frameIndex))
                {
                    previousFrameIndex += IsRewinding ? -1 : 1;
                    PerformUpdates(updatesByFrame[previousFrameIndex], elapsedTime, IsRewinding);
                }

                PerformUpdates(updatesByFrame[previousFrameIndex], elapsedTime, IsRewinding);
            }
        }

        private void PerformUpdates(IEnumerable<IGameUpdate> updates, float elapsedTime, bool isRewinding)
        {
            foreach (IGameUpdate update in updates)
            {
                if (isRewinding)
                {
                    update.Revert(elapsedTime);
                }
                else
                {
                    update.Perform(elapsedTime);
                }
            }
        }

        private void RaisePropertyChanged(string name)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}
